#include <iostream>
#include <map>
#include <sys/time.h>
#include <cstdlib>
#include <Network.hpp>

#define DEBUG false

using namespace std;

using namespace Network;
using namespace RegularExpressions;
using namespace System;



void printError(const string& msg)
{
	cout << flush;
	#ifdef _WIN32
		CONSOLE_SCREEN_BUFFER_INFO con_info;
        HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
		GetConsoleScreenBufferInfo(hConsole, &con_info);
		WORD attrBase = con_info.wAttributes;
		
		SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_INTENSITY);
		cout << msg << endl;
		SetConsoleTextAttribute(hConsole, attrBase);
	#else
		cout << "\033[31m" << msg << "\033[0m" << endl;
	#endif
}



void printWarning(const string& msg)
{
	cout << flush;
	#ifdef _WIN32
		CONSOLE_SCREEN_BUFFER_INFO con_info;
        HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
		GetConsoleScreenBufferInfo(hConsole, &con_info);
		WORD attrBase = con_info.wAttributes;
		
		SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << msg << endl;
		SetConsoleTextAttribute(hConsole, attrBase);
	#else
		cout << "\033[33m" << msg << "\033[0m" << endl;
	#endif
}



class ClientThread : public Thread
{
	protected:
	
	static map<string, IpAddress> listHost;
	
	TcpSocket _csock;
	TcpSocket _csockHost;
	Regex _requestType;
	Regex _contentLen;
	Regex _host;
	Regex _transferEnc;
	Regex _proxyConnectField;
	Regex _proxyUrl;
	Mutex _mput;
	Mutex _mListHost;
	
	
	public:
	
	ClientThread(const TcpSocket& sock)
	 : _csock(sock),
	   _requestType("^[^\r\n ]+", REG_EXTENDED | REG_ICASE), 
	   _contentLen("\r\ncontent-length *: *([0-9]+)\r\n", REG_EXTENDED | REG_ICASE), 
	   _host("\r\nhost *: *([^\r\n :]+)(:[1-9][0-9]*)?\r\n", REG_EXTENDED | REG_ICASE),
	   _transferEnc("\r\ntransfer-encoding *: *chunked", REG_EXTENDED | REG_ICASE),
	   _proxyConnectField("\r\nproxy-connection *: *[^\r\n]+", REG_EXTENDED | REG_ICASE),
	   _proxyUrl("^([^\r\n ]+) [a-z]+://[^/ \r\n]+/?", REG_EXTENDED | REG_ICASE)
	{
		
	}
	
	
	protected:
	
	void getRequest(ByteArray& request, string& host, int& port, int& contentSize)
	{
		putMsg("Attente d'une requête...");
		
		_csock.recv(request, "\r\n\r\n");
		request += "\r\n\r\n";
		
		debug("Request : \n" + request.toString());
		
		string strRequest = request.toString();
		Match m1 = _requestType.match(strRequest);
		Match m3 = _host.match(strRequest);
		Match m4 = _proxyConnectField.match(strRequest);
		Match m5 = _proxyUrl.match(strRequest);
		
		if(!m1.success())
			throw runtime_error("Bad request : Can't get request type");
		
		string requestType = m1.group(0).value();
		
		if(requestType != "POST" && requestType != "GET")
			throw runtime_error("Bad request : Unknow request type '" + requestType + "'");
		
		if(requestType == "POST")
		{
			Match m2 = _contentLen.match(strRequest);
			
			if(!m2.success())
				throw runtime_error("Bad request : POST request without content-length field");
			
			contentSize = atoi(m2.group(1).value().c_str());
		}
		else
		{
			contentSize = 0;
		}
		
		if(!m3.success())
			throw runtime_error("Bad request : Can't find host field request");
		
		host = m3.group(1).value();
		
		if(m3.countGroups() == 3)
			port = atoi(m3.group(2).value().c_str()+1);
		else
			port = 80;
		
		
		if(m4.success())
		{
			Group g = m4.group(0);
			request = request.left(g.start()) + request.right(request.size()-g.end());
		}
		
		if(m5.success())
			request = m5.group(1).value() + " /" + request.right(request.size()-m5.group(0).end());
		
		debug("Final request : \n" + request.toString());
	}
	
	void connectToHost(const string& host, int port)
	{
		IpAddress ipHost("0.0.0.0");
		
		_mListHost.lock();
		map<string, IpAddress>::iterator it = listHost.find(host);
		
		if(it != listHost.end())
		{
			ipHost = it->second;
		}
		else
		{
			ipHost = Dns::getHostAddresses(host)[0];
			listHost.insert(pair<string, IpAddress>(host, ipHost));
		}
		_mListHost.unlock();
		
		putMsg("Connexion à l'hôte distant '" + host + "'...");
		
		_csockHost.connect(IpEndPoint(ipHost, port));
	}
	
	void sendRequest(const ByteArray& request)
	{
		_csockHost.send(request);
	}
	
	void transmitPostData(int contentSize)
	{
		putMsg("Transmission des données POST...");
		
		int lenRecv = contentSize;
		ByteArray buff(4096);
		
		do
		{
			_csock.recv(buff);
			_csockHost.send(buff);
			lenRecv -= buff.size();
		}
		while(lenRecv > 0);
	}
	
	void transmitResponse()
	{
		ByteArray header;
		ByteArray buff;
		
		putMsg("Attente du header de la réponse...");
		
		_csockHost.recv(buff, "\r\n\r\n");
		buff += "\r\n\r\n";
		
		debug("Response header : \n" + buff.toString());
		
		_csock.send(buff);
		
		string strHeader = buff.toString();
		Match m1 = _contentLen.match(strHeader);
		Match m2 = _transferEnc.match(strHeader);
		
		if(m1.success())
		{
			putMsg("Transmission des données (not chunked)...");
			
			int contentSize = atoi(m1.group(1).value().c_str());
			
			if(contentSize > 0)
			{
				int lenRecv = contentSize;
				buff.resize(4096);
				
				do
				{
					_csockHost.recv(buff);
					_csock.send(buff);
					lenRecv -= buff.size();
				}
				while(lenRecv > 0);
			}
		}
		else if(m2.success())
		{
			putMsg("Transmission des données (chunked)...");
			
			int chunkDataSize = 0;
			
			do
			{
				_csockHost.recv(buff, "\r\n");
				_csock.send(buff + "\r\n");
				
				sprintf(&buff.toString()[0], "%X", chunkDataSize);
				
				if(chunkDataSize != 0)
				{
					_csockHost.recv(buff, chunkDataSize);
					_csock.send(buff+"\r\n");
					_csockHost.recv(buff, 2);
				}
			}
			while(chunkDataSize > 0);
			
			do
			{
				_csockHost.recv(buff, "\r\n");
				_csock.send(buff + "\r\n");
			}
			while(buff != "");
		}
		else
		{
			throw runtime_error("Bad response : No data to transmit");
		}
	}
	
	void run()
	{
		try
		{
			const IpEndPoint& ep = _csock.remoteEndPoint();
			putMsg("A client has joined the server from " + ep.ip());
			
			while(true)
			{
				ByteArray request;
				int contentSize;
				string host;
				int port;
				
				//struct timeval tv[2];
				//gettimeofday(&tv[0], NULL);
				getRequest(request, host, port, contentSize);
				connectToHost(host, port);
				sendRequest(request);
				//gettimeofday(&tv[1], NULL);
				
				if(contentSize > 0)
					transmitPostData(contentSize);
				
				transmitResponse();
				
				//int us = (tv[1].tv_sec - tv[0].tv_sec) * 1000000 + tv[1].tv_usec - tv[0].tv_usec;
				//cout << "\t\t\t" << us << endl;
			}
		}
		catch(exception& e)
		{
			error(string("Client error : ") + e.what());
		}
		
		delete this;
	}
	
	
	void putMsg(const string& msg)
	{
		_mput.lock();
		cout << msg << endl;
		_mput.unlock();
	}
	
	void error(const string& msg)
	{
		_mput.lock();
		printError(msg);
		_mput.unlock();
	}
	
	void warning(const string& msg)
	{
		_mput.lock();
		printWarning(msg);
		_mput.unlock();
	}
	
	void debug(const string& msg)
	{
		if(DEBUG)
		{
			_mput.lock();
			cout << msg << endl;
			_mput.unlock();
		}
	}
};



map<string, IpAddress> ClientThread::listHost;



int main()//int argc, char* argv[]
{
	try
	{
		load();
		
		
		TcpSocket sock;
		
		cout << "Liaison de la socket..." << endl;
		sock.bind(IpEndPoint(IpAddress::ipv4_any, 1080));
		
		cout << "Mise en écoute du port" << endl;
		sock.listen(5);
		
		cout << "Serveur démarré avec succès" << endl;
		
		while(true)
		{
			ClientThread* thread = new ClientThread(sock.accept());
			thread->start();
		}
		
		sock.finish();
		
		
		unload();
	}
	catch(exception& e)
	{
		printError(string("Critical error : ") + e.what());
	}
}




